iOS 符号解析重构之路
1.1 什么是符号解析
所谓的符号解析就是就是将崩溃日志中的地址映射成为可读的符号和源文件中的行号,方便开发者定位和修复问题。如下图,第一份完全不可读的崩溃日志经过完整的符号解析变成了第三份完全可读的日志。对于字节的稳定性监控平台而言,需要支持 iOS 端的崩溃/卡死 /卡顿/自定义异常等各种日志类型的反解,因此符号解析也是监控平台必备的一项底层基础能力。
1.2 系统原生符号解析工具
symbolicatecrash
Xcode 提供的 symbolicatecrash
。该命令位于:/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
,是一个perl 脚本,里面整合了逐步解析的操作(也可以将命令拷贝出来,直接进行调用)。
用法:symbolicatecrash log.crash -d xxx.app.dSYM
优点:能非常方便的符号化整份 crash 日志。
缺点:
耗时比较久。
粒度比较粗,无法符号化特定的某一行。
atos
用法:atos -o xxx.app.dSYM/Contents/Resources/DWARF/xxx -arch arm64/armv7 -l loadAddress runtimeAddress
优点:速度快,可以符号化特定的某一行,方便上层做缓存。
1.3 原生工具的问题
但是上面的这两个工具都有两个最大的缺陷就是:
都仅仅是单机的工具,无法作为在线服务提供。 必须依赖 macOS 系统,因 为字节服务端基建全部基于Linux,导致无法复用集团各种平台和框架,这就带来了非常高的机器成本,部署成本和运维成本。
二、历史方案探索
为了解决这两大痛点,搭建一套 Linux 上可以提供 iOS 在线符号解析的服务,历史上我们依次做了如下探索:
方案1:llvm-atosl
这套方案起初没有太大的问题,但是随着时间的推移,晚高峰期间经常出现因为解析超时导致解析失败进而只能看到地址偏移而看不到符号的问题,因此还需要找到瓶颈再进一步优化。
方案2:llvm-atosl-cgo
cgo
而不是命令行形式调用。too many open files
报错,当时怀疑到是fd占用超过上限,又联想到每次执行 llvm-atosl 脚本会占用至少 3 个 fd(stdin,stdout和stderr),因此我们尝试将 llvm-atosl 从命令行工具的形式封装为一个c的library,再通过cgo
在 golang 侧调用:package main
/*
#cgo CFLAGS: -I./tools
#cgo LDFLAGS: -lstdc++ -lncurses -lm -L${SRCDIR}/tools/ -lllvm-atosl
#include "llvm-atosl-api.h"
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"strconv"
"strings"
"unsafe"
)
func main() {
result = symbolicate("~/dsym/7.8.0(78007)eb7dd4d73df0329692003523fc2c9586/Aweme.app.dSYM/Contents/Resources/DWARF/Aweme","arm64","0x100008000","0x0000000102cff4b8");
fmt.Println(result)
}
func symbolicate(go_path string, go_arch string, go_loadAddress string, go_address string) string {
c_path := C.CString(go_path)
c_arch := C.CString(go_arch)
loadAddress := hex2int(go_loadAddress)
c_loadAddress := C.ulong(loadAddress)
address := hex2int(go_address)
c_address := C.ulong(address)
c_result := C.getSymbolicatedName(c_path, c_arch, c_loadAddress, c_address)
result := C.GoString(c_result)
C.free(unsafe.Pointer(c_path))
C.free(unsafe.Pointer(c_arch))
C.free(unsafe.Pointer(c_result))
return result;
}
func hex2int(hexStr string) uint64 {
// remove 0x suffix if found in the input string
cleaned := strings.Replace(hexStr, "0x", "", -1)
// base 16 for hexadecimal
result, _ := strconv.ParseUint(cleaned, 16, 64)
return uint64(result)
}
cgo
性能不佳的两大原因:线程的栈在 Go 运行时是比较少的,受到 P(Processor,可以理解为 goroutine 的管理调度者)以及 M(Machine,可以理解为物理线程)数量的限制,一般可以简单的理解成受到 GOMAXPROCS
限制,go 1.5 版本之后的GOMAXPROCS
默认是机器 CPU 核数,因此一旦cgo
并发调用的方法数量超过GOMAXPROCS
,就会发生调用阻塞。由于需要同时保留 C/C++ 的运行时, cgo
需要在两个运行时和两个 ABI(抽象二进制接口)之间做翻译和协调。这就带来了很大的开销。
方案3:golang-atos
基于 golang 原生的系统库debug/dwarf
,可以实现对 DWARF 文件的解析,将地址解析为符号,可以替换 llvm-atosl 的实现,并且可以天然利用 golang 协程的特性实现高并发。实现方案可以参考下面这段源码:
package dwarfexample
import (
"debug/macho"
"debug/dwarf"
"log"
"github.com/go-errors/errors")
func ParseFile(path string, address int64) (err error) {
var f *macho.FatFile
if f, err = macho.OpenFat(path); err != nil {
return errors.New("open file error: " + err.Error())
}
var d *dwarf.Data
if d, err = f.Arches[1].DWARF(); err != nil {
return
}
r := d.Reader()
var entry *dwarf.Entry
if entry, err = r.SeekPC(address); err != nil {
log.Print("Not Found ...")
return
} else {
log.Print("Found ...")
}
log.Printf("tag: %+v, lowpc: %+v", entry.Tag, entry.Val(dwarf.AttrLowpc))
var lineReader *dwarf.LineReader
if lineReader, err = d.LineReader(entry); err != nil {
return
}
var line dwarf.LineEntry
if err = lineReader.SeekPC(0x1005AC550, &line); err != nil {
return
}
log.Printf("line %+v:%+v", line.File.Name, line.Line)
return
}
三、终极解决方案
3.1 方案整体设计
将符号和地址的映射从崩溃时查找对应的符号表文件调用命令行工作解析改成了符号表文件上传时全量预解析所有地址与符号的映射关系,然后将映射关系结构化存储,崩溃时查找缓存即可。 为了解决部分 C++ 与 Rust 符号 demangle 失效以及各种语言 demangle 工具不一致的问题。将原本 llvm 自带的 demangle 工具替换成了一个 Rust 实现,支持全语言的 demangle 工具 symbolic-demangle(链接见参考资料[2]),极大的降低了运维成本。 优先采用新方案做符号解析,新方案没命中放量或者新方案解析失败用老方案做兜底。
3.2 方案实现细节
3.2.1 符号表文件格式
DWARF
文件结构
DWARF 是一种调试信息格式,通常用于源码级别调试,也可用于从运行时地址还原源码对应的符号以及行号的工具(如: atos)。
DWARF with dSYM
之后,Xcode 会生成一个 dSYM 文件,其中显式包含 DWARF 从而帮助我们根据地址,找到方法符号及文件名和行号等信息,方便开发者在版本正式发布之后排查问题。MH_DSYM
。既然是 Mach-O 文件,使用 size 命令可以查看 AwemeDylib 这个 DWARF 文件中包含的 Segment 和 Section,以 arm64 架构为例:~/Downloads/dwarf/AwemeDylib.framework.dSYM/Contents/Resources/DWARF > size -x -m -l AwemeDylib
AwemeDylib (for architecture arm64):
Segment __TEXT: 0x18a4000 (vmaddr 0x0 fileoff 0)
Section __text: 0x130fd54 (addr 0x5640 offset 0)
Section __stubs: 0x89d0 (addr 0x1315394 offset 0)
Section __stub_helper: 0x41c4 (addr 0x131dd64 offset 0)
Section __const: 0x1a4358 (addr 0x1321f40 offset 0)
Section __objc_methname: 0x47c15 (addr 0x14c6298 offset 0)
Section __objc_classname: 0x45cd (addr 0x150dead offset 0)
Section __objc_methtype: 0x3a0e6 (addr 0x151247a offset 0)
Section __cstring: 0x1bf8e4 (addr 0x154c560 offset 0)
Section __gcc_except_tab: 0x1004b8 (addr 0x170be44 offset 0)
Section __ustring: 0x1d46 (addr 0x180c2fc offset 0)
Section __unwind_info: 0x67c40 (addr 0x180e044 offset 0)
Section __eh_frame: 0x2e368 (addr 0x1875c88 offset 0)
total 0x189e992
Segment __DATA: 0x5f8000 (vmaddr 0x18a4000 fileoff 0)
Section __got: 0x4238 (addr 0x18a4000 offset 0)
Section __la_symbol_ptr: 0x5be0 (addr 0x18a8238 offset 0)
Section __mod_init_func: 0x1850 (addr 0x18ade18 offset 0)
Section __const: 0x146cb0 (addr 0x18af670 offset 0)
Section __cfstring: 0x1b2c0 (addr 0x19f6320 offset 0)
Section __objc_classlist: 0x1680 (addr 0x1a115e0 offset 0)
Section __objc_nlclslist: 0x28 (addr 0x1a12c60 offset 0)
Section __objc_catlist: 0x208 (addr 0x1a12c88 offset 0)
Section __objc_protolist: 0x2f0 (addr 0x1a12e90 offset 0)
Section __objc_imageinfo: 0x8 (addr 0x1a13180 offset 0)
Section __objc_const: 0xb2dc8 (addr 0x1a13188 offset 0)
Section __objc_selrefs: 0xf000 (addr 0x1ac5f50 offset 0)
Section __objc_protorefs: 0x48 (addr 0x1ad4f50 offset 0)
Section __objc_classrefs: 0x16a8 (addr 0x1ad4f98 offset 0)
Section __objc_superrefs: 0x1098 (addr 0x1ad6640 offset 0)
Section __objc_ivar: 0x42c4 (addr 0x1ad76d8 offset 0)
Section __objc_data: 0xe100 (addr 0x1adb9a0 offset 0)
Section __data: 0xc0d20 (addr 0x1ae9aa0 offset 0)
Section HMDModule: 0x50 (addr 0x1baa7c0 offset 0)
Section __bss: 0x1e9038 (addr 0x1baa820 offset 0)
Section __common: 0x1058e0 (addr 0x1d93860 offset 0)
total 0x5f511c
Segment __LINKEDIT: 0x609000 (vmaddr 0x1e9c000 fileoff 4096)
Segment __DWARF: 0x2a51000 (vmaddr 0x24a5000 fileoff 6332416)
Section __debug_line: 0x3e96b7 (addr 0x24a5000 offset 6332416)
Section __debug_pubnames: 0x16ca3a (addr 0x288e6b7 offset 10434231)
Section __debug_pubtypes: 0x2e111a (addr 0x29fb0f1 offset 11927793)
Section __debug_aranges: 0xf010 (addr 0x2cdc20b offset 14946827)
Section __debug_info: 0x12792a4 (addr 0x2ceb21b offset 15008283)
Section __debug_ranges: 0x567b0 (addr 0x3f644bf offset 34378943)
Section __debug_loc: 0x674483 (addr 0x3fbac6f offset 34733167)
Section __debug_abbrev: 0x2637 (addr 0x462f0f2 offset 41500914)
Section __debug_str: 0x5d0e9e (addr 0x4631729 offset 41510697)
Section __apple_names: 0x1a6984 (addr 0x4c025c7 offset 47609287)
Section __apple_namespac: 0x1b90 (addr 0x4da8f4b offset 49340235)
Section __apple_types: 0x137666 (addr 0x4daaadb offset 49347291)
Section __apple_objc: 0x13680 (addr 0x4ee2141 offset 50622785)
total 0x2a507c1
total 0x4ef6000
__DWARF
的 Segment, 下面包含 __debug_line
, __debug_aranges
, __debug_info
等很多类 Section。我们可以使用dwarfdump
来探索DWARF段中的内容,例如输入命令dwarfdump AwemeDylib --debug-info
可展示__debug_info
Section 下已经格式化之后的内容。关于dwarfdump
指令的完整用法可以参考 llvm 工具链的官方文档(链接见参考资料[3])。debug_info
debug_info
section 是 DWARF 文件中最核心的信息。DWARF 用The Debugging Information Entry
(DIE) 来以统一的形式描述这些信息,每个 DIE 包含:一个 TAG 属性表达描述什么类型的元素, 如: DW_TAG_subprogram
(函数)、DW_TAG_formal_parameter
(形式参数)、DW_TAG_variable
(变量)、DW_TAG_base_type
(基础类型)。N 个属性(attribute), 用于具体描述一个 DIE。
0x0049622c: DW_TAG_subprogram
DW_AT_low_pc (0x000000000030057c)
DW_AT_high_pc (0x0000000000300690)
DW_AT_frame_base (DW_OP_reg29 W29)
DW_AT_object_pointer (0x0049629e)
DW_AT_name ("+[SSZipArchive _dateWithMSDOSFormat:]")
DW_AT_decl_file ("/var/folders/03/2g9r4cnj3kqb5605581m1nf40000gn/T/cocoapods-uclardjg/Pods/SSZipArchive/SSZipArchive/SSZipArchive.m")
DW_AT_decl_line (965)
DW_AT_prototyped (0x01)
DW_AT_type (0x00498104 "NSDate*")
DW_AT_APPLE_optimized (0x01)
DW_AT_low_pc
,DW_AT_high_pc
分别代表函数的起始/结束 PC 地址。DW_AT_name
描述函数的名字为 +[SSZipArchive _dateWithMSDOSFormat:]。DW_AT_decl_file
指的是这个函数在.../SSZipArchive.m 文件中声明。DW_AT_decl_line
指的是这个函数在.../SSZipArchive.m 文件第 965 行声明。DW_AT_type
描述的是函数的返回值类型,对于这个函数来说,为 NSDate*。
DWARF 只有有限种类的属性, 全部属性的列表可以参考 llvm api 文档(链接见参考资料[5])中 DW_TAG 开头的部分。
DW_AT_low_pc
和DW_AT_high_pc
描述的机器码地址不等价于程序在运行时的地址,我们可以称之为 file_address。操作系统基于安全因素的考虑,会应用一种地址空间布局随机化的技术 ASLR,加载可执行文件到内存时,会做一个随机偏移(下文中用 load_address 代指),我们获取到偏移后还需要加上__TEXT
Segment 的 vmaddr 才可以还原出运行时地址。vmaddr 可以通过上面的size指令或者otool -l
指令拿到。注意vmaddr一般跟架构有着直接的关系,对于 armv7 架构而言通常是0x4000,对于 arm64 架构而言通常是 0x100000000,但是也不绝对,例如这里放的 AwemeDylib 动态库符号表 arm64 架构的 vmaddr 就是 0。我们将函数在 App 运行时的地址称之为 runtime_address。
file_address = runtime_address - load_address + vm_address
CompileUnit
DW_TAG_compile_unit
的 DIE。编译单元代表的是一个可执行源文件编译后的__TEXT
和__DATA
等产物,一般可以简单的理解为我们代码中的一个参与编译的文件,例如.m,.mm,.cpp,.c等不同编程语言对应的源文件。一个编译单元包含在这个编译单元中声明的所有DIE(包括方法,参数,变量等)。举一个典型的例子:0x00495ea3: DW_TAG_compile_unit
DW_AT_producer ("Apple LLVM version 10.0.0 (clang-1000.11.45.5)")
DW_AT_language (DW_LANG_ObjC)
DW_AT_name ("/var/folders/03/2g9r4cnj3kqb5605581m1nf40000gn/T/cocoapods-uclardjg/Pods/SSZipArchive/SSZipArchive/SSZipArchive.m")
DW_AT_stmt_list (0x001e8f31)
DW_AT_comp_dir ("/private/var/folders/03/2g9r4cnj3kqb5605581m1nf40000gn/T/cocoapods-uclardjg/Pods")
DW_AT_APPLE_optimized (0x01)
DW_AT_APPLE_major_runtime_vers (0x02)
DW_AT_low_pc (0x00000000002fc8e8)
DW_AT_high_pc (0x0000000000300828)
DW_AT_language
,描述的是当前编译单元使用的是哪种编程语言。DW_AT_stmt_list
指的是当前编译单元对应的行号信息在debug_line
section 中的偏移,在下一小结中我们再详细介绍。DW_AT_low_pc
,DW_AT_high_pc
这里分别代表编译单元包含的所有DW_TAG_subprogram
TAG 的 DIE 的整体的起始/结束的 PC 地址。
debug_line
dwarfdump AwemeDylib --debug-line
可以查看到debug_line
section 结构化之后的数据。DW_AT_stmt_list
,也就是0x001e8f31
:debug_line[0x001e8f31]
...
include_directories[ 1] = "/var/folders/03/2g9r4cnj3kqb5605581m1nf40000gn/T/cocoapods-uclardjg/Pods/SSZipArchive/SSZipArchive"
...
file_names[ 1]:
name: "SSZipArchive.m"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
...
Address Line Column File ISA Discriminator Flags
------------------------ ------ ------ --- ----- ------------- --------
0x00000000002fc8e8 46 0 1 0 0 is_stmt
0x00000000002fc908 48 32 1 0 0 is_stmt prologue_end
0x00000000002fc920 0 32 1 0 0
0x00000000002fc928 48 19 1 0 0
0x00000000002fc934 49 9 1 0 0 is_stmt
0x00000000002fc938 53 15 1 0 0 is_stmt
0x00000000002fc940 54 9 1 0 0 is_stmt
...
0x0000000000300828 1058 1 1 0 0 is_stmt end_sequence
include_directories
和file_names
组合起来就是参与编译文件的绝对路径。Address:这里指的是 FileAddress。
Line: 指的是 FileAddress 在源文件中对应的行号。
Column:FileAddress 在源文件中对应的列号。
File:源文件 index,与上面 file_names 中的下标是一致的。
ISA:无符号整数,指的是当前指令适用于哪些指令集架构,这里一般都是 0。
Discriminator:无符号整数,标志当前的指令在多编译单元中的归属,在单编译单元的体系中一般是 0。
Flags:一些标记位,这里解释其中最重要的两个:
end_sequence:是目标文件机器指令结束地址+1,所以可以认为在当前编译单元中,只有 end_sequence 对应地址之前的地址才是有效的指令。 is_stmt:表示当前指令是否为推荐的断点位置,一般而言 is_stmt 为 false 的代码可能对应的是编译器优化后的指令,这部分的指令一般行号都是 0,对我们分析问题是有干扰的,下文中会讲如何校正。
符号解析原理
比如这行调用栈:
5 AwemeDylib 0x000000010035d580 0x10005d000 + 3147136
0x10005d000 - 0x1000dffff AwemeDylib arm64
file_address = 0x000000010035d580 - 0x10005d000 + 0x0 = 0x300580
dwarfdump --lookup
指令可以查找出对应的方法名和行号:dwarfdump
从地址到符号映射的原理(atos 等其他工具同理):dwarfdump
解析的结果与我们手动人肉解析的结果也是完全一致的,下图中 0x30057c~0x300593 这个地址范围解析出来的文件名和行号都是完全一致的。func_name (in binary_name) (file_name:line_number)
+[SSZipArchive _dateWithMSDOSFormat:] (in AwemeDylib) (SSZipArchive.m:965)
dwarf atos -o AwemeDylib.framework.dSYM/Contents/Resources/DWARF/AwemeDylib -arch arm64 -l 0x10005d000 0x000000010035d580 +[SSZipArchive _dateWithMSDOSFormat:] (in AwemeDylib) (SSZipArchive.m:965)
Symbol Table
如果被静态链接的 Framework 在打包的时候将编译参数 GCC_GENERATE_DEBUGGING_SYMBOLS
改成 NO,那么最终 App 打包时候生成的 dSYM 文件将没有这部分代码生成机器指令对应的文件名和行号信息。对于系统库而言,并没有提供 dSYM 文件,我们有的仅仅是.dylib 或者 .framework 等格式的 MachO 文件,例如 libobjc.A.dylib
,Foundation.framework
等。
Symbol Table String
来进行符号解析。文件结构
String Table Index:就是 String 表中的偏移量。通过这个偏移量可以访问到符号对应的具体字符串,例如上图中圈中的第一个 symbol info 的偏移量是 0x0048C12B,再加上 String Table 的起始地址 0x02BBC360 ,等于 0x304848B。查询之后果然是 _ff_stream_add_bitstream_filter。
value:当前方法对应的起始的 FileAddress。
符号解析原理
对 Symbol Table 列表的 value 排序。
将 value 排好序,查找到刚刚好小于 value的index,则崩溃的信息就存在于 index-1下标的数据区中,再用 index-1 下标数据区中的 String Table Index 就可以在 String Table 索引到对应的方法名。然后 FileAddress - 目标数据区的 value 就是崩溃地址距离方法起始地址的偏移字节数。
func_name (in binary_name) + func_offset
_ff_stream_add_bitstream_filter (in AwemeDylib) + 2
dwarf atos -o AwemeDylib.framework.dSYM/Contents/Resources/DWARF/AwemeDylib -arch arm64 -l 0x0 0x56C1DE ff_stream_add_bitstream_filter (in AwemeDylib) + 2
3.2.2 线上预解析方案实现
Golang 原生实现
debug/dwarf
解析 DWARF 文件 ,可以非常方便的打印出 address 对应的文件名及行号,而 Golang 天然的就支持跨平台。debug/dwarf
并没有提供直接解析方法名的 api,这就导致解析结果不完整。对于内联函数的文件名和行号等更加复杂的场景也没有兼容。
这里的实现其实还是基于已知 FileAddress 的前提,并没有提供全量预解析的方案。
仅支持 Dwarf 文件的解析,不支持 Symbol Table 的解析。
全量预解析实现
__TEXT
Segment 中的__text
Section可能出现的地址范围逐一解析出来,然后存到后端的分布式缓存比如 Hbase 或者 redis 不就好了吗?+[SSZipArchive _dateWithMSDOSFormat:] (in AwemeDylib) (SSZipArchive.m:965)
。这样就起码有了 20 倍的压缩率,而且这个策略无论对 DWARF 文件还是 Symbol Table 而言都是适用的。hbase_key = [table_name]+image_name+uuid+chunk_index
table_name:用于区分dwarf和 symbol_table 两种类型。
image_name:binary 的名字,例如 Aweme,libobjc.A.dylib 等。
uuid:一个符号表文件的唯一标示,注意一般 dSYM 为多架构的胖二进制文件,而不同架构的 MachO 文件 uuid 也不同。
chunk_index:指的是以连续长度为一个常数 N(这里以 10000 为例)的地址空间为单位切分,计算当前地址能落到哪个下标中,也可以认为是当前地址除以常数N然后向下取整。对于单个地址而言是非常明确的,但是对于一段地址范围的话就比较复杂了,如果一段地址范围的下限和上限除以常数 N 向下取整相同的话,他们就落在相同的下标中,但是如果不同的话,为了保证读取的时候落到这一段地址范围中的每个地址都能够被正确的解析,因此地址范围首尾横跨的所有 chunk_index,都需要写入该地址范围。
DWARF 文件解析
全量 CompileUnit 解析
debug_info
section 中的偏移地址都保存在debug_arranges
section 中。debug_arranges
binary 中的结构,基于文档中的结构,我们需要把所有的debug_info_offset
都手动解析出来,因为篇幅的原因这里就不贴代码实现了,需要特别留意一点的就是 binary 手动解析的时候一定要留意大小端。地址全量解析流程
内联函数函数名还是以函数的声明为准,但是文件名和行号要以被内联的位置为准,这与 atos 的解析结果是一致的。否则连续的两层调用栈信息就可能出现跳跃,影响分析问题的效率。 从《DWARF 文件格式官方文档》中我们可以了解到, debug_line
中Flags那一列如果有is_stmt
的话,表示当前指令是编译器推荐的断点位置,否则对应的指令就是编译器自动生成的编译器推荐的断点位置。因为断点只可以打在同一行,那么我们可以判断出从有is_stmt
flag 的那行指令到下一次有is_stmt
flag 的这若干行指令对应的源码文件名和行号都是完全相同的,那么针对没有is_stmt
flag 的那行指令,我们只需要找到挨得最近,且地址比它小,且有is_stmt
flag 的那行信息,就可以准确的获取到对应地址解析后的文件名和行号。所以总结一下结论就是:debug_line 连续几行的行号信息是否可以合并的标志就是is_stmt
,只有连续两行is_stmt
为 true 之间的的 debug line info 才可以被合并。这里写入到 Hbase 中的地址范围指的是偏移地址,计算公式是:offset = file_address - __TEXT.vmaddr。这样在解析的时候就不需要关心对应 DWARF 文件的 __TEXT
Segment 的起始地址。
Symbol Table 解析
3.2.3 踩坑记
写入耗时远远大于预期。
问题原因:在写入 Hbase 之前调用了 demangle 工具,每一次都有额外几十ms的性能开销,在量级夸张的情况下这个问题会被放大。
解决方案:将 demangle 的时机从 Hbase 写入之前改到了从 Hbase 查询之后,毕竟崩溃的方法比起全量的方法而言还是少得多得多。
CompileUnit 获取失败。
问题原因:绝大部分情况下,从.debug_arranges section 中取出的 compile unit offset 需要手动加一个 0xB 的偏移才刚好是我们预期的 CompileUnit 的偏移。 但是在这个case就出现的意外:
首先我们看到它的偏移并不是 0xB,而且从 debug_arranges section 中取出的 compile unit offset 就直接是正确的了,原因暂时未知。
解决方案:做一个兼容,如果加上 0xB 的 offset 取 compile unit 出错的话,那就减去 0xB 再重试一次。debug_line 中连续两行出现了一模一样的地址,导致解析结果有歧义。
问题原因:虽然连续两行地址相同,但是文件名和行号却不一致,这就导致了结果有歧义。
解决方案:参考 atos 的解析结果,以前面的那一行为准。 debug_line
已经读到end_sequence
那行也就是最后那行,但是当前 CompileUnit 还有一部分 TAG 为DW_TAG_subprogram
的 DIE 没有被debug_line
中的任何地址索引到。那么这一部分地址范围就被漏掉了。问题原因:怀疑与编译器优化有关,这部分 DIE 的方法名一般都是以
_OUTLINED_FUNCTION_
开头。解决方案:如果已经解析完
end_sequence
那行,当前CompileUnit还有TAG为DW_TAG_subprogram
的DIE没被索引到,那么这部分DIE地址范围对应的文件名和行号就是end_sequence
这行的的文件名和行号。Symbol Table 中出现非法数据。 问题原因:Symbol Table 中这条数据的 FileAddress 居然比 __TEXT.vmaddr 还要小,这就导致 offset 变成负数了,又因为一开始对地址偏移我们定义的是 uint_64 类型,导致 offset 被强转成了一个特别大的整数,不符合预期。
解决方案:过滤掉地址偏移为负数的数据段。
四、上线效果
4.1 单行解析耗时
7.7 10:46 最近 6h 平均耗时优化了 70倍,pct99 300多倍
4.2 crash接口整体耗时
4.3 符号表文件访问量级
4.4 解析报错
4.5 物理机性能
选取线上一台比较有代表性的物理机监控,可以看到机器负载,内存占用,CPU 占用,网络 IO 同比都有非常明显的优化。
下面截取部分核心指标优化前和优化后的指标看板作对比:
优化前时间范围: 7.3 12:00 - 7.5 12:00
优化后时间范围: 7.10 12:00 - 7.12 12:00
15min 负载
IOWait CPU 占用
内存占用
网络 Input 流量
参考资料
[1] https://my.oschina.net/linker/blog/1529928
[2] https://docs.rs/crate/symbolic-demangle/8.3.0
[3] https://llvm.org/docs/CommandGuide/llvm-dwarfdump.html
[4] http://www.dwarfstd.org/doc/DWARF4.pdf